异步编程:JS语言的执行环境是单线程的,因此异步编程对JS的可用性非常重要
| 异步编程方式 | 兼容性 |
|---|---|
| 回调函数 | 完全兼容 |
| 事件监听 | 完全兼容 |
| 发布/订阅 | 完全兼容 |
Promise 对象 |
es6+ |
Async 函数 |
es7+ |
16.1 基本概念
16.1.1 异步
| 异步和同步 | 说明 |
|---|---|
| 异步 | 一个任务不连续分两段执行(第二段在回调中),中间可以插入其它任务 |
| 同步 | 连续执行,不能插入其他任务 |
16.1.2 回调函数
说明:JS语言对异步编程的实现,就是回调函数(就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数)
注意:执行分成两段,在这两段之间抛出的错误,程序无法捕捉,只能当作参数,传入第二段。所以,Node.js约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是null)
Demo: 读取文件
1. 向操作系统发出请求,要求读取文件
2. 程序执行其他任务
3. 等到操作系统返回文件,再接着执行任务的第二段(处理文件)
1 | fs.readFile('/etc/passwd', function (err, data) { |
16.1.3 Promise
说明:它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用,解决多重回调函数嵌套导致的callback hell问题
分析:Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了
缺点:代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚
Demo: 连续读取多个文件
1 | var readFile = require('fs-readfile-promise'); |
16.2 Generator函数
16.2.1 协程
说明:协程是一种多任务的解决方案,有点像函数,又有点像线程。es6是通过Generator函数实现协程的。
Generator函数:遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作
16.2.2 Generator函数的概念
说明:Generator函数是协程在ES6的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
1 | function* gen(x){ |
16.2.3 Generator函数的数据交换和错误处理
数据交换
next方法返回值的value属性,是Generator函数向外输出数据next方法还可以接受参数,这是向Generator函数体内输入数据
1 | function* gen(x){ |
错误处理
- 通过
throw()方法在外部抛出内部错误 - 通过
try...catch捕获内部错误
1 | function* gen(x){ |
16.2.4 异步任务的封装
分析:虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)
Demo: 执行一个真实的异步任务
1 | var fetch = require('node-fetch'); |
16.3 Thunk函数
说明:并不是es6/es7提供的API
16.3.1 参数的求值策略
传值调用(call by value)
说明:函数被调用后,在进入函数体之前,就计算参数的表达式的值,再将这个值传入函数
- 实现比较简单
- 有可能造成性能损失
采纳者举例:c语言、JavaScript语言
传名调用(call by name)
说明:直接将作为参数的表达式传入函数体,只在用到它的时候求值
实现方式:编译器的传名调用实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。
采纳者举例:Haskell语言
16.3.2 Thunk 函数的含义
说明:Thunk函数是传名调用的一种实现策略,用来替换某个表达式。
Demo:JS 的调用过程是传值调用,下面的例子模拟了传名调用
一段平淡无奇的 JS
1 | var x = 4; |
和上面等价的传名调用过程
1 | var thunk = function () { |
16.3.3 JavaScript 语言的 Thunk 函数
说明:在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
Demo1: 基本应用
正常版本的readFile(多参数版本)
1 | fs.readFile(fileName, callback); |
Thunk版本的readFile(单参数版本)
1 | var Thunk = function (fileName){ |
通用 Thunk 函数转换器
说明:任何函数,只要参数有回调函数,就可以通过这个通用的 Thunk 函数转换器转换为一个 Thunk 函数。本质上,就是一个特殊的柯里化过程。
ES5版本
1 | var Thunk = function(fn){ |
ES6版本
1 | var Thunk = function(fn) { |
应用
1 | function f(a, cb) { |
16.3.4 Thunkify 模块
说明:一个第三方的 Thunk 函数转换器。
安装
1 | $ npm install thunkify |
源码
说明:源码主要多了一个检查机制,确保返回的回调函数只运行一次
1 | function thunkify(fn){ |
使用
Demo1: 读取文件
1 | var thunkify = require('thunkify'); |
Demo2: 演示回调只被执行一次
1 | function f(a, b, callback){ |
16.3.5 Generator 函数的流程管理
说明:Thunk函数
1 | var fs = require('fs'); |
16.3.6 Thunk函数的自动流程管理
说明:如果Generator函数处理多个同步操作,自动流程管理就非常简单。但异步的情况就会复杂一些,必须有一种机制,自动控制Generator函数的流程,接收和交还程序的执行权。解决方案有两类
(1)回调函数。将异步操作包装成Thunk函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成Promise对象,用then方法交回执行权。
Demo:异步读取文件:Thunk 函数 + Generator函数 + 递归处理
yield命令用于将程序的执行权移出Generator函数- 在回调函数里,将执行权交还给
Generator函数
1 | var fs = require('fs'); |
16.4 co模块
说明:co模块是著名程序员TJ Holowaychuk于2013年6月发布的一个小工具,用于Generator函数的自动执行。
16.4.1 基本用法
co函数
说明:Generator函数只要传入co函数,就会自动执行
返回值:返回一个Promise对象,因此可以用then方法添加回调函数
注意:co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。
1 | var co = require('co'); |
16.4.2 co模块的原理
说明:就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。
16.4.3 基于Promise对象的自动执行
说明:和基于Thunk函数的自动执行器的区别,仅仅是利用Promist.prototype.then替换了回调
Demo:异步读取文件
1 | var fs = require('fs'); |
16.4.3 co模块的源码
1 | function co(gen) { |
16.5 async函数
兼容性: ES7提案阶段,转码器 Babel 和 regenerator 都已经支持
16.5.1 含义
说明: async 函数是 Generator 函数的语法糖,进一步说,async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖。
语法:将 Generator 函数的星号 * 替换成 async,将 yield 替换成 await
相比 Generator函数的改进
- 内置执行器:
async函数的执行,与普通函数一模一样 - 更好的语义:
async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果 - 更广的适用性:
co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作) - 返回值是
Promise:async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了。你可以用then方法指定下一步的操作
Demo
说明:使用Generator异步读取文件
Generator 版本
1 | var fs = require('fs'); |
async 版本
1 | var asyncReadFile = async function (){ |
16.5.2 语法
16.5.2.1 返回值
说明: async 函数返回一个 Promise 对象
async函数内部return语句返回的值,会成为then方法回调函数的参数(传递给哪个回调函数取决于async返回的Promise对象的状态)- 必须等到内部所有
await命令的Promise对象执行完,才会发生状态改变(就是说,只有async函数内部的异步操作全部执行完,才会执行then方法指定的回调函数) await命令后面表达式的返回值如果不是一个Promise对象,会被转成一个立即resolve的Promise对象
16.5.2.2 return
说明:async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数
1 | async function f() { |
16.5.2.3 返回值(Promise 对象)的状态
说明:必须等到内部所有 await 命令的 Promise 对象执行完,才会发生状态改变
状态为 rejected 的场景
说明:分两种情况,且两种情况都会导致整个 async 函数中断执行
async函数内部抛出错误(且reject的参数会被catch方法的回调函数接收到)- 只要有一个
await后面的运算返回的Promise状态为rejected
技巧:这两种情况都可以通过将异常捕获来使 async 函数不至于中断,异常捕获方式看后面
Demo: async 函数内部抛出错误
1 | async function f() { |
状态为 resolved 的场景
- 所有
await后面的运算返回的Promise状态为resolved
emphasized text
1 | async function getTitle(url) { |
16.5.2.4 错误处理
说明: async 函数的语法规则总体上比较简单,难点是错误处理机制。一共有两类错误,它们都会使 async 停止,并使返回的 Promise 状态改变为 rejected
async函数内部抛出错误(且reject的参数会被catch方法的回调函数接收到)- 只要有一个
await后面的运算返回的Promise状态为rejected
处理方式:有两种方式来处理
try...catch: 将可能出现异常的await放在try...catch结构里面catch():await后面的Promise对象再跟一个catch方面,处理前面可能出现的错误
注意:错误得到处理,则 async 的运行就不会被中断
Demo1: try…catch
1 | async function main() { |
Demo2: catch()
1 | async function f() { |
16.5.3 async 函数的实现
说明: 用 es5-的方式实现 async 函数的功能
es5版:通过 Generator 和自动执行函数模拟 async
1 | /** |
es7版
1 | async function fn(args){ |
16.5.4 async 函数的用法
16.5.4.1 定义 async 函数
说明:多重场景下定义 async 函数的方式
- 函数声明
- 函数表达式
- 对象的方法
- 尖头函数
1 | // 函数声明 |
16.5.4.2 调用 async 函数
运行逻辑: async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回 Promise 对象(但状态要等所有异步操作完成改变),等到触发的异步操作完成,再接着执行函数体内后面的语句。
1 | async function getStockPriceByName(name) { |
16.5.5 注意点
(1)最好把 await 命令放在 try...catch 代码块中或使用 catch(),从而保证 async 函数会完成所有的异步操作
Demo1: try…catch 方式
1 | async function myFunction() { |
Demo2: catch() 方式
1 | async function myFunction() { |
(2)多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发(可以使用 Promise.all()),缩短程序的执行时间
Demo1: Promise.all()
1 | async function dbFuc(db) { |
Demo2: 先启动所有异步操作,再对每个 Promise 对象使用 await 监控其状态
1 | async function dbFuc(db) { |
(3)await 命令只能用在 async 函数之中,如果用在普通函数,就会报错
1 | async function dbFuc(db) { |
16.5.6 与 Promise、Generator 的比较
说明:通过一个案例进行比较
案例:某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值
Promise 实现
优点:比回调函数的写法大大改进
缺点:代码完全都是 Promise 的 API(then 、 catch等等),操作本身的语义反而不容易看出来
1 | function chainAnimationsPromise(elem, animations) { |
Generator 实现
优点:语义比 Promise 写法更清晰
缺点
- 必须有一个任务运行器,自动执行
Generator函数 - 必须保证
yield语句后面的表达式,必须返回一个Promise
1 | function chainAnimationsGenerator(elem, animations) { |
async 实现
优点:实现最简洁,最符合语义
1 | async function chainAnimationsAsync(elem, animations) { |
16.6 异步遍历器
背景: Iterator接口提供的 next()调用后需要同步地取得 {value, done},但如果执行的是异步操作,无法同步地取得 {value, done}。
目前的解决方法(以 Generator 为例): Generator 函数里面的异步操作,返回一个 Thunk 函数或者 Promise 对象,即 value 属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而 done 属性则还是同步产生的
异步遍历器:为异步操作提供原生的遍历器接口,即 value 和 done 这两个属性都是异步产生
兼容性:提案阶段
16.6.1 异步遍历的接口
部署:部署在 Symbol.asyncIterator 属性上
使用:异步便利器的 next() 返回一个 Promise 对象,可以在随后的 then 中提供回调获取 value 和 done
1 | // asyncIterator 是一个便利器对象 |
16.6.2 for await…of
说明:用于遍历异步的 Iterator 接口
1 | // readLines函数返回一个异步遍历器,每次调用它的next方法,就会返回一个Promise对象。await表示等待这个Promise对象resolve,一旦完成,变量line就是Promise对象返回的value值 |
16.6.3 异步Generator函数
说明:返回一个异步遍历器对象
语法: async 函数与 Generator 函数的结合
注意:普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。
1 | async function* readLines(path) { |